package gamemodels;

import gamecomponents.Player;
import gamecomponents.Timer;
import gamecomponents.graphic.BlitData;
import gamecomponents.graphic.TextureHelper;
import gamecomponents.settings.GameSettings;
import input.InputManager;
import java.awt.event.KeyEvent;
import java.util.LinkedList;
import java.util.List;
import java.util.Observable;

import maze.Map;
import maze.Maze;
import maze.MazeSize;
import maze.MazeStructure;

import com.threed.jpct.CollisionEvent;
import com.threed.jpct.CollisionListener;
import com.threed.jpct.Config;
import com.threed.jpct.Lights;
import com.threed.jpct.Loader;
import com.threed.jpct.Object3D;
import com.threed.jpct.Primitives;
import com.threed.jpct.SimpleVector;
import com.threed.jpct.Texture;
import com.threed.jpct.TextureManager;
import com.threed.jpct.World;

/**
 * A Single player game session.
 * @author Team Deer
 *
 */
public class SinglePlayerModel extends Observable implements IGameModel,
		WorldModel {

	private static SinglePlayerModel singletonInstance;

	private IGameModel nextModel;

	// Set to true to stop running model
	private boolean requestExit;

	// The visual representation of a world
	private final World world;

	private final static float worldSize = 10f;

	// Size of the screen used to draw HUD.
	private final SimpleVector screenSize;

	// MazeStructure
	private MazeStructure mazeStructure;

	// Maze
	private Maze maze;
	private MazeSize mazeSize;

	// Map
	private Map map;
	private boolean showMap;

	private Object3D finishLineSensor;
	private final boolean gameFinished = false;

	private final Object3D skyDomeModel;

	private final List<BlitData> blitDataList = new LinkedList<BlitData>();

	// Player
	private Player player;
	
	// Previous shown second
	private int previousTimerSecond = 0;

	// Timer
	private Timer timer;

	// Constructor and singleton getter
	
	private SinglePlayerModel() {

		// Store screensize
		this.screenSize = GameSettings.SCREEN_SIZE;

		this.world = new World();
		this.initWorld();

		// Loading the sky
		this.skyDomeModel = Loader.load3DS("Content\\Models\\skydomeModel.3DS",
				1)[0];
		final Texture skyDomeTexture = new Texture(
				"Content\\Models\\Textures\\skyTexture.png");
		TextureManager.getInstance().addTexture("skydomeTexture",
				skyDomeTexture);
		this.skyDomeModel.setTexture("skydomeTexture");

		Config.maxPolysVisible = 8192;

		// Load textures for showing the timer
		this.loadTimeTextures();

		// Load colonTexture
		final Texture colonTexture = TextureHelper
				.loadTexture("Content\\Sprites\\Singleplayer\\colonTexture.png");
		TextureManager.getInstance().addTexture("colonTexture", colonTexture);
		
		// Load transparent texture
		TextureManager.getInstance().addTexture("transparent", 
				TextureHelper.getTransparentTexture());

	}

	/**
	 * 
	 * @return the Singleton instance
	 */
	public static SinglePlayerModel getInstance() {
		if (singletonInstance == null) {
			singletonInstance = new SinglePlayerModel();
		}
		return singletonInstance;
	}

	// Update
	
	/**
	 * Some configurations to the world.
	 */
	private void initWorld() {
		/*
		 * Setup the lighting. We are not using overbright lighting because the
		 * OpenGL renderer can't do it, but we are using RGB-scaling. Some
		 * hardware/drivers for OpenGL don't support this (but most do).
		 */
		Config.fadeoutLight = false;
		this.world.getLights().setOverbrightLighting(
				Lights.OVERBRIGHT_LIGHTING_DISABLED);
		this.world.getLights().setRGBScale(Lights.RGB_SCALE_2X);
		this.world.setAmbientLight(50, 55, 60);

		Config.tuneForOutdoor();

		// this.world.setFogging(World.FOGGING_ENABLED);
		// this.world.setFogParameters(1200, 0, 0, 0);
		Config.farPlane = 1000 * worldSize;
	}

	/**
	 * Load textures used by View
	 * to blit the timer.
	 */
	// Methods
	private void loadTimeTextures() {
		for (int i = 0; i < 10; i++) {
			final Texture texture = TextureHelper
					.loadTexture("Content\\Sprites\\NumbersSprites\\num" + i
							+ ".png");
			TextureManager.getInstance().addTexture("num" + i, texture);
		}
	}

	/**
	 * Starts a new game.
	 * 
	 * @param width
	 * @param height
	 */
	public void createNewGame(final MazeSize mazeSize, final boolean useOldMap) {

		if (useOldMap){
			
			// Resets position
			this.map.setPlayerState(new SimpleVector(), 0);
			
			// Recreate Map to draw in the HUD
			this.map = new Map(this.mazeStructure, new SimpleVector(), 0);
			
			final float playerHeight = -worldSize / 5f;
			this.player = new Player(this.world, new SimpleVector(0, playerHeight,
					0), (float) Math.PI, 0, playerHeight);
			this.player.setMaxSpeed(GameSettings.PLAYER_MAX_SPEED);
			
			this.timer = new Timer(GameSettings.MS_PER_FRAME);
			this.timer.startTimer();
			super.setChanged();
		
			return;
		}
		
		this.mazeSize = mazeSize; 
		
		int width = mazeSize.getWidth();
		int height = mazeSize.getHeight();
		
		// Clean world
		this.world.removeAll();

		// Add light sources
		this.addLights();

		// Construct a new maze
		this.mazeStructure = new MazeStructure(width, height);
		this.maze = new Maze(this.world, this.mazeStructure);

		this.addFinishLine();

		// Create Map to draw in the HUD
		this.map = new Map(this.mazeStructure, new SimpleVector(), 0);

		final float playerHeight = -worldSize / 5f;
		this.player = new Player(this.world, new SimpleVector(0, playerHeight,
				0), (float) Math.PI, 0, playerHeight);

		this.player.setMaxSpeed(GameSettings.PLAYER_MAX_SPEED);

		this.skyDomeModel.setScale(Math.max(width, height));
		this.skyDomeModel.translate(width * worldSize, 0, height * worldSize);
		this.skyDomeModel.build();
		this.world.addObject(this.skyDomeModel);

		this.timer = new Timer(GameSettings.MS_PER_FRAME);
		this.timer.startTimer();
		super.setChanged();
	}

	/**
	 * Places some lightsources
	 */
	private void addLights() {
		this.world.addLight(new SimpleVector(11 * worldSize, -7.5 * worldSize,
				11 * worldSize), 15, 12, 9);
		this.world.addLight(new SimpleVector(11 * worldSize, -5 * worldSize,
				-worldSize), 10, 8, 6);
		this.world.addLight(new SimpleVector(-worldSize, -5 * worldSize,
				11 * worldSize), 10, 8, 6);
		this.world.addLight(new SimpleVector(-worldSize, -2.5 * worldSize,
				-worldSize), 5, 4, 3);
	}

	/**
	 * Constructs the finish line by 
	 * checking collision with a box 
	 * placed at the finishline
	 */
	private void addFinishLine() {
		this.finishLineSensor = Primitives.getBox(worldSize / 2, .7f);
		this.finishLineSensor.build();

		this.finishLineSensor.setTransparency(0);
		this.finishLineSensor.setTexture("transparent");

		this.finishLineSensor.rotateY((float) (Math.PI / 4));
		this.finishLineSensor.translate(this.maze.getFinishLinePosition());

		this.finishLineSensor.setCollisionMode(Object3D.COLLISION_CHECK_OTHERS);
		this.finishLineSensor.addCollisionListener(new CollisionListener() {

			private static final long serialVersionUID = 3327281982267221618L;

			@Override
			public boolean requiresPolygonIDs() {
				return false;
			}

			@Override
			public void collision(final CollisionEvent arg0) {

				SinglePlayerModel.this.timer.stopTimer();
				SinglePlayerModel.this.changeModel(MazeFinishedModel
						.getInstance());
			}
		});
		this.world.addObject(this.finishLineSensor);
	}

	/**
	 * 
	 */
	@Override
	public void reset() {
		this.requestExit = false;
		this.nextModel = null;
		this.showMap = false;
	}

	/**
	 * Indicates whether the model should continue to run.
	 */
	@Override
	public boolean isRunning() {
		return !this.requestExit;
	}

	/**
	 * The update call.
	 */
	@Override
	public void update(final InputManager inputManager) {
		if (inputManager.getKeyboardState().isKeyPressed(KeyEvent.VK_ESCAPE) &&
				inputManager.getKeyboardState().getPreviousKeyboardState().isKeyUp(KeyEvent.VK_ESCAPE)) {
			changeModel(MainMenuModel.getInstance());
			this.timer.stopTimer();
			super.setChanged();
		}

		if (inputManager.getKeyboardState().isKeyPressed(KeyEvent.VK_P) &&
				inputManager.getKeyboardState().getPreviousKeyboardState().isKeyUp(KeyEvent.VK_P)) {
			if (!this.timer.isRunning()) {
				this.timer.resumeTimer();
			} else {
				this.timer.pauseTimer();
			}
			super.setChanged();
		}

		// Is game is paused, don't update anything else
		if (!this.timer.isRunning()) {
			this.endUpdate(inputManager);
			return;
		}

		// Show Map
		if (this.showMap != inputManager.getKeyboardState().isKeyPressed(
				KeyEvent.VK_Q)) {
			this.showMap = !this.showMap;
			super.setChanged();
		}

		// Update Player
		if (this.player.update(inputManager)) {
			super.setChanged();
			this.map.updatePlayerPosition(this.player.getPlayerPosition(),
					this.player.getPlayerRotationY());
		}

		// Tick timer
		if (this.previousTimerSecond != this.timer.getSeconds()) {
			super.setChanged();
			this.previousTimerSecond = this.timer.getSeconds();
		}

		this.endUpdate(inputManager);
	}

	private void endUpdate(final InputManager inputManager) {
		super.notifyObservers();
	}
	
	private void changeModel(final IGameModel model) {
		this.nextModel = model;
	}

	@Override
	public IGameModel getNextModel() {
		return this.nextModel;
	}
	
	/**
	 * @return the current maze size
	 */
	public MazeSize getMazeSize() {
		return this.mazeSize;
	}

	/**
	 * @return The timer
	 */
	public Timer getTimer() {
		return this.timer;
	}

	/**
	 * @return the world in which the maze exists.
	 */
	@Override
	public World getWorld() {
		return this.world;
	}
	
	// Viewhelp
	
	/**
	 * Describes how to draw time.
	 * 
	 * @return List with BlitData to draw overlay
	 */
	private List<BlitData> getTimeData() {
		final List<BlitData> timeDataList = new LinkedList<BlitData>();

		final int digitWidth = 32;
		final int digitHeight = 42;
		final int lineSpacing = 4;

		// Draw colon
		final Texture colonTexture = TextureManager.getInstance().getTexture(
				"colonTexture");
		timeDataList.add(new BlitData(
				colonTexture, // texture
				0 /* srcX */, 0 /* srcY */,
				(int) (this.screenSize.x - digitWidth) / 2 /* destX */,
				(int) (.1f * this.screenSize.y) /* destY */, colonTexture
						.getWidth() /* sourceWidth */,
				colonTexture.getHeight() /* sourceHeight */,
				digitWidth /* destWidth */, digitHeight /* destHeight */,
				255 /* transValue */, false /* additive */, null)); /* addColor */

		// Draw seconds
		final int[] s = { this.timer.getSeconds() / 10,
				this.timer.getSeconds() % 10 };
		for (int i = 0; i < 2; i++) {
			final Texture texture = TextureManager.getInstance().getTexture(
					"num" + s[i]);
			timeDataList.add(new BlitData(
					texture, // texture
					0 /* srcX */, 0 /* srcY */,
					(int) (this.screenSize.x + digitWidth) / 2 + (i + 1)
							* lineSpacing + i * digitWidth /* destX */,
					(int) (.1f * this.screenSize.y) /* destY */, texture
							.getWidth() /* sourceWidth */,
					texture.getHeight() /* sourceHeight */,
					digitWidth /* destWidth */, digitHeight /* destHeight */,
					255 /* transValue */, false /* additive */, null)); /* addColor */
		}

		// Draw minutes
		final int[] m = { this.timer.getMinutes() % 10,
				this.timer.getMinutes() / 10 };
		for (int i = 0; i < 2; i++) {
			final Texture texture = TextureManager.getInstance().getTexture(
					"num" + m[i]);
			timeDataList.add(new BlitData(
					texture, // texture
					0 /* srcX */, 0 /* srcY */,
					(int) (this.screenSize.x - digitWidth) / 2 - (i + 1)
							* lineSpacing - (i + 1) * digitWidth /* destX */,
					(int) (.1f * this.screenSize.y) /* destY */, texture
							.getWidth() /* sourceWidth */,
					texture.getHeight() /* sourceHeight */,
					digitWidth /* destWidth */, digitHeight /* destHeight */,
					255 /* transValue */, false /* additive */, null)); /* addColor */
		}

		return timeDataList;
	}

	/**
	 * Describes how to draw the map.
	 * 
	 * @return List with BlitData to draw overlay
	 */
	private List<BlitData> getMapData() {
		final List<BlitData> list = new LinkedList<BlitData>();

		this.map.drawMap();

		list.add(new BlitData(
				this.map.getTexture(), // texture
				0 /* srcX */, 0 /* srcY */, 0 /* destX */, 0 /* destY */, this.map
						.getWidth() /* sourceWidth */,
				this.map.getHeight() /* sourceHeight */,
				this.map.getWidth() /* destWidth */,
				this.map.getHeight() /* destHeight */, 255 /* transValue */,
				false /* additive */, null)); /* addColor */

		return list;
	}

	/**
	 * @return the whole list of data to blit in View.
	 */
	@Override
	public List<BlitData> getBlitData() {

		this.blitDataList.clear();

		if (!this.gameFinished) {

			if (this.showMap) {
				this.blitDataList.addAll(getMapData());
			}

			this.blitDataList.addAll(getTimeData());
		}

		return this.blitDataList;
	}
	
}
